/policy-bot

A GitHub App that enforces approval policies on pull requests

Primary LanguageGoApache License 2.0Apache-2.0

policy-bot

policy-bot is a GitHub App for enforcing approval policies on pull requests. It does this by creating a status check, which can be configured as a required status check.

While GitHub natively supports required reviews, policy-bot provides more complex approval features:

  • Require reviews from specific users, organizations, or teams
  • Apply rules based on the files, authors, or branches involved in a pull request
  • Combine multiple approval rules with and and or conditions
  • Automatically approve pull requests that meet specific conditions

Behavior is configured by a file in each repository. policy-bot also provides a UI to view the detailed approval status of any pull request.

Configuration

By default, the behavior of the bot is configured by a .policy.yml file at the root of the repository. The file name and location are configurable when running your own instance of the server.

  • If the file does not exist, the policy-bot status check is not posted. This means it is safe to enable policy-bot on all repositories in an organization.
  • The .policy.yml file is read from most recent commit on the target branch of each pull request.

policy.yml Specification

The overall policy is expressed by:

  • Lists of rule definitions
  • A set of policies that combine the rules or define additional options

Consider the following example, which allows changes to certain paths without review, but all other changes require review from the devtools/infrastructure. Any member of the devtools organization can also disapprove changes.

# the high level policy
policy:
  approval:
    - or:
      - the infrastructure team has approved
      - only staging files have changed
  disapproval:
    requires:
      organizations:
        - "devtools"

# the list of rules
approval_rules:
  - name: the infrastructure team has approved
    requires:
      count: 1
      teams:
        - "devtools/infrastructure"
  - name: only staging files have changed
    if:
      only_changed_files:
        paths:
          - "staging/.*"
    requires:
      count: 0

Remote Policy Configuration

You can also define a remote policy by specifying a repository, path, and ref (only repository is required). Instead of defining a policy key, you would define a remote key. Only 1 level of remote configuration is supported by design.

# The remote repository to read the policy file from. This is required, and must
# be in the form of "org/repo-name". Must be a public repository.
remote: org/repo-name

# The path to the policy config file in the remote repository. If none is
# specified, the default path in the server config is used.
path: path/to/policy.yml

# The branch (or tag, or commit hash) that should be used on the remote
# repository. If none is specified, the default branch of the repository is used.
ref: master

Approval Rules

Each list entry in approval_rules has the following specification:

# "name" is required, and is used to reference rules in the "policy" block
name: "example rule"

# "if" specifies a set of predicates that must be true for the rule to apply.
# This block, and every condition within it are optional. If the block does not
# exist, the rule applies to every pull request.
if:
  # "changed_files" is satisfied if any file in the pull request matches any
  # regular expression in the list.
  changed_files:
    paths:
      - "config/.*"
      - "server/views/.*\\.tmpl"

  # "only_changed_files" is satisfied if all files changed by the pull request
  # match at least one regular expression in the list.
  only_changed_files:
    paths:
      - "config/.*"

  # "has_author_in" is satisified if the user who opened the pull request is in
  # the users list or belongs to any of the listed organizations or teams.
  has_author_in:
    users: ["user1", "user2", ...]
    organizations: ["org1", "org2", ...]
    teams: ["org1/team1", "org2/team2", ...]

  # "has_contributor_in" is satisfied if any commits on the pull request have
  # an author or committer in the users list or that belong to any of the
  # listed organizations or teams.
  has_contributor_in:
    users: ["user1", "user2", ...]
    organizations: ["org1", "org2", ...]
    teams: ["org1/team1", "org2/team2", ...]

  # "targets_branch" is satisfied if the target branch on the pull request
  # matches the regular expression
  targets_branch:
    pattern: "^(master|regexPattern)$"

# "options" specifies a set of restrictions on approvals. If the block does not
# exist, the default values are used.
options:
  # If true, approvals by the author of a pull request are considered when
  # calculating the status. False by default.
  allow_author: false

  # If true, the approvals of someone who has committed to the pull request are
  # considered when calculating the status. False by default.
  allow_contributor: false

  # If true, pushing new commits to a pull request will invalidate existing
  # approvals for this rule. False by default.
  invalidate_on_push: false

  # "methods" defines how users may express approval. The defaults are below.
  methods:
    comments:
      - ":+1:"
      - "👍"
    github_review: true

# "requires" specifies the approval requirements for the rule. If the block
# does not exist, the rule is automatically approved.
requires:
  # "count" is the number of required approvals. The default is 0, meaning no
  # approval is necessary.
  count: 1

  # A user must be in the list of users or belong to at least one of the given
  # organizations or teams for their approval to count for this rule.
  users: ["user1", "user2"]
  organizations: ["org1", "org2"]
  teams: ["org1/team1", "org2/team2"]

Approval Policies

The approval block in the policy section defines a list of rules that must all be true:

policy:
  approval:
    - rule1
    - rule2
    - rule3
    - ...

Each list entry may be the name of a rule, or one of the following conjunctions:

or:
  - rule1
  - rule2
  - ...
and:
  - rule1
  - rule2
  - ...

Conjunctions can contain more conjunctions (up to a maximum depth of 5):

- or:
    - rule1
    - rule2
    - and:
        - rule3
        - rule4

Disapproval

Disapproval allows users to explicitly block pull requests if certain changes must be made. Any member of in the set of allowed users can disapprove a change or revoke another user's disapproval.

Unlike approval, all disapproval options are specified as part of the policy. Effectively, there is a single disapproval rule. The disapproval policy has the following specification:

# "disapproval" is the top-level key in the policy block.
disapproval:
  # "options" sets behavior related to disapproval. If it does not exist, the
  # defaults shown below are used.
  options:
    # "methods" defines how users set and revoke disapproval.
    methods:
      # "disapprove" sets the methods for disapproval.
      disapprove:
        comments:
          - ":-1:"
          - "👎"
        github_review: true

      # "revoke" sets the methods for revoking disapproval. Usually, these will
      # match the methods used by approval rules.
      revoke:
        comments:
          - ":+1:"
          - "👍"
        github_review: true

  # "requires" sets the users that are allowed to disapprove. If it is not set,
  # disapproval is not enabled.
  requires:
    users: ["user1", "user2"]
    organizations: ["org1", "org2"]
    teams: ["org1/team1", "org2/team2"]

Caveats and Notes

There are several additional behaviors that follow from the rules above that are worth mentioning.

Disapproval is Disabled by Default

You must set at least one of the disapproval.requires fields to enable disapproval. Without setting one of these fields, GitHub reviews that request changes have no effect on the policy-bot status.

or, and, and if (Rule Predicates)

If the if block of a rule (the predicate) is not satisfied, the rule is marked as "skipped". Skipped rules interact with or and and as follows:

  • An and block containing only skipped rules is also skipped
  • An or block containing only skipped rules is also skipped

Effectively, skipped rules are treated as if they don't exist.

Cross-organization Membership Tests

policy-bot allows approval rules to reference organizations and teams that are not in the organization that owns the repository where the rules appear. In this case, policy-bot must be installed on all referenced organizations.

Deployment

policy-bot is easy to deploy in your own environment as it has no dependencies other than GitHub. It is also safe to run multiple instances of the server, making it a good fit for container schedulers like Nomad or Kubernetes.

We provide both a Docker container and a binary distribution of the server. A sample configuration file is provided at config/policy-bot.example.yml. We recommend deploying the application behind a reverse proxy or load balancer that terminates TLS connections.

GitHub App Configuration

policy-bot requires the following permissions as a GitHub app:

  • Issues - read-only
  • Repository metadata - read-only
  • Pull requests - read-only
  • Single file - read-only, with the file matching the server configuration
  • Commit status - read & write
  • Organization members - read-only

It should be subscribed to the following events:

  • Issue comment
  • Pull request
  • Status
  • Pull request review

There is a logo.png provided if you'd like to use it as the GitHub application logo. The background color is #4d4d4d.

Operations

policy-bot uses go-baseapp and go-githubapp, both of which emit standard metrics and structured log keys. Please see those projects for details.

Development

To develop policy-bot, you will need a Go installation.

Run style checks and tests

./godelw verify

Running the server locally

# copy and edit the server config
cp config/policy-bot.example.yml config/policy-bot.yml

./godelw run policy-bot server
  • config/policy-bot.yml is used as the default configuration file
  • The server is available at http://localhost:8080/

Running the server via docker

# copy and edit the server config
cp config/policy-bot.example.yml config/policy-bot.yml

# build the docker image
./godelw docker build --verbose

docker run --rm -v "$(pwd)/config:/secrets/" -p 8080:8080 palantirtechnologies/policy-bot:latest
  • This will mount the path relative path config/ which should contain the modified config file policy-bot.yml
  • The server is available at http://localhost:8080/

Example Policy Files

Example policy files can be found in config/policy-examples

Contributing

Contributions and issues are welcome. For new features or large contributions, we prefer discussing the proposed change on a GitHub issue prior to a PR.

License

This library is made available under the Apache 2.0 License.