Have custom review rules for Pull Requests, assigning a given amount of code owners and required requests to different areas of the code.
This action is intended for the case where a repository needs to have a more custom review criteria. This is not a replacement for CODEOWNERS
, but an enhancement of it.
It allows sensitive files to be reviewed by a fixed number of users.
You declare that the file example.rs
belongs to team-rs
in the CODEOWNERS
file, and you require 3 approvals to merge a Pull Request.
If 1 user belonging to team-rs
approves the PR, and two different users also approve the PR, this will satisfy the requirements for GitHub.
With Review-Bot
you can request that 3 users from the team-rs
must review that file.
There is a file protected in CODEOWNERS
and requires reviews from both team-abc
and team-xyz
.
If one user belongs to both teams, their review will satisfy both requirements.
With the and-distinct
rule, you can request that two distinct users must review that file.
The installation requires three files for it to work, we first need to create a file (.github/review-bot.yml
by default).
rules:
- name: General
condition:
include:
- '.*'
type: basic
teams:
- your-team-name-here
The second file is the triggering file. This will be .github/workflows/review-trigger.yml
:
name: Review-Trigger
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- review_requested
- review_request_removed
- ready_for_review
pull_request_review:
jobs:
trigger-review-bot:
runs-on: ubuntu-latest
name: trigger review bot
steps:
- name: Get PR number
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
echo "Saving PR number: $PR_NUMBER"
mkdir -p ./pr
echo $PR_NUMBER > ./pr/pr_number
- uses: actions/upload-artifact@v3
name: Save PR number
with:
name: pr_number
path: pr/
retention-days: 5
And then we must create a final file. This will be .github/workflows/review-bot.yml
.
name: Review Bot
on:
workflow_run:
workflows:
- Review-Trigger
types:
- completed
permissions:
contents: read
checks: write
pull-requests: write
jobs:
review-approvals:
runs-on: ubuntu-latest
steps:
- name: Extract content of artifact
id: number
uses: Bullrich/extract-text-from-artifact@main
with:
artifact-name: pr_number
- name: "Evaluates PR reviews"
uses: paritytech/review-bot@main
with:
repo-token: ${{ github.token }}
team-token: ${{ secrets.TEAM_TOKEN }}
checks-token: ${{ secrets.CHECKS_TOKEN }}
request-reviewers: false
pr-number: ${{ steps.number.outputs.content }}
Create a new PR and see if it is working.
You should see a new action running called Review PR/review-approvals
.
Wait for it to finish.
After this, go to your branch protection rules and make sure that you have the following setup enabled:
- Require status checks to pass before merging
- Status checks that are required.
review-bot
If review-bot
does not appear, make a new PR and wait for review-approvals
action to finish. This will create the status and now it should be available in your list.
Use pull_request_target
for the event, not pull_request
.
- This is a security measure so that an attacker doesn’t have access to our secrets.
We use worflow_run
to let the action be triggered at all times (If we don’t use this, GitHub will stop it if it comes from a fork).
By chaining events we are able to safely execute our action without jeopardizing our secrets. You can even use environment
in the final file if you want to have it extra secure.
Review bot has multiple configurations available. It has available inputs and outputs. It also has rule configurations, but you can find that in the Rule Configuration section.
You can find all the inputs in the action file, but let's walk through each one of them:
repo-token
: Token to access to the repository.- required
- This is provided by the repo, you can simply use
${{ github.token }}
. - It is already in the installation section, but you need to give the following permissions to the action:
contents
: readchecks
: writepull-requests
: write
- Imporant: if
request-reviewers
is enabled and you are using teams, this needs to be a GitHub Personal Access token withrepo
andread:org
scopes.- This is because GitHub actions does not have access to the teams, and it can not assign teams as reviewers.
team-token
: Token to read the team members.- required if you are using teams.
- If you are only using individual users, you can use
{{ github.token }}
instead.
- If you are only using individual users, you can use
- This needs to be a GitHub Personal Access token with
read:org
permission. - It is used to extract the members of teams.
- required if you are using teams.
checks-token
: Token to write the status checks.- required.
- This needs to be a GitHub Personal Access token with
checks:write
permission. - It is used to write the status checks of successful/failed runs.
- Can be
${{ github.token }}
but there is a known bug.- If you use a GitHub app, this bug will be fixed.
- You can use the same GitHub app for
checks-token
andteam-token
.
- You can use the same GitHub app for
- If you use a GitHub app, this bug will be fixed.
config-file
: The location of the config file.- default:
.github/review-bot.yml
- default:
request-reviewers
: If the system should automatically request the required reviewers.- default: false.
- If enabled, when there are missing reviews, the system will request the appropriate users and/or team to review.
- If enabled, and using teams, this requires a GitHub app with
write
permission forpull request
.
In some cases, specially in big organizations, it is more organized to use a GitHub app to authenticate, as it allows us to give it permissions per repository, and we can fine-grain them even better. If you wish to do that, you need to create a GitHub app with the following permissions:
- Organization permissions:
- Members
- Read
- Members
- Repository permissions:
- Checks
- Write
- Pull Request
- Write
- Checks
Because this project is intended to be used with a token, we need to do an extra step to generate one from the GitHub app:
- After you create the app, copy the App ID and the private key and set them as secrets.
- Then you need to modify the workflow file to have an extra step:
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v1.9.3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: "Evaluates PR reviews"
uses: paritytech/review-bot@main
with:
# The previous step generates a token which is used as the input for this action
repo-token: ${{ steps.generate_token.outputs.token }}
team-token: ${{ steps.generate_token.outputs.token }}
checks-token: ${{ steps.generate_token.outputs.token }}
pr-number: ${{ steps.number.outputs.content }}
Outputs are needed for your chained actions. If you want to use this information, remember to set an id
field in the step, so you can access it.
You can find all the outputs in the action file, but let's walk through each one of them:
repo
: Organization and repo name. Written in the format ofowner/repo
.report
: This is ajson
object with the report of the evaluation.
interface Report {
// State of the evaluation.
conclusion: "action_required" | "failure" | "success";
// list of files that are being modified by the PR
files: string[];
// Array of rules that has not been fulfilled.
// If this is empty it means that it has enough approvals
report: {
/** The name of the rule */
name: string;
/** The amount of missing reviews to fulfill the requirements */
missingReviews: number;
/** The users who would qualify to complete those reviews */
missingUsers: string[];
/** If applicable, the teams that should be requested to review */
teamsToRequest?: string[];
/** If applicable, the users that should be requested to review */
usersToRequest?: string[];
}[];
}
This is the file where all the available rules are written.
This file is only read from the main branch. So if you modify the file, the changes won’t happen until it is merged into the main branch. This is done to stop users from modifying the rules in their PRs.
It contains an object called rules
which has an array of rules. Every rule has a same base structure. There is also a second optional field called preventReviewRequests
.
rules:
- name: Rule name
condition:
include:
- '.*'
exclude:
- 'README.md'
type: the type of the rule
preventReviewRequests:
users:
- user-a
- user-b
teams:
- team-a
- team-b
- name: Name of the rule. This value must be unique per rule.
- condition: This is an object that contains two values:
- include: An array of regex expressions of the files that match this rule.
- If any of the files modified in a Pull Request matches this regex, the rule will be evaluated.
- If no file matches the regex, then this rule won’t be evaluated.
- exclude: An array of regular expressions pointing to the files that this rule should ignore when deciding if it should evaluate the Pull Request.
- optional
- Useful if, for example, a whole directory is inside a rule, but you want to ignore a particular file as it doesn’t need specific reviewers.
- type: This is the type of the rule.
- The available types are:
- basic: Just requires a given number of reviews.
- or: Has many review options, requires at least one option to be fulfilled.
- and: Has many review options, requires all the options to be fulfilled.
- and-distinct: Has many review options, requires all the options to be fulfilled by different people.
- The available types are:
- include: An array of regex expressions of the files that match this rule.
This is a special field that applies to all the rules.
This field is optional and currently not used. Pending on #53
Every type has a slightly different configuration and works for different scenarios, so let’s analyze all of them.
As the name implies, this type is elementary. All the files that fall under the rule evaluation must receive a given number of approvals by the listed users and/or team members.
A minimal full configuration file is:
rules:
- name: General
condition:
include:
- '.*'
exclude:
- '.github/'
type: basic
minApprovals: 2
teams:
- team-1
- team-2
users:
- user-1
- user-2
countAuthor: true
allowedToSkipRule:
teams:
- team-1
users:
- user-1
It has the same parameters as a normal rule:
- name: Name of the rule. This value must be unique per rule.
- condition: This is an object that contains two values:
- include: An array of regex expressions of the files that match this rule.
- exclude: An array of regular expressions pointing to the files that this rule should ignore when deciding if it should evaluate the Pull Request.
- optional
- type: This must be the string
basic
. - minApprovals: The number of approvals that are need to fulfill this condition.
- It can not be lower than 1.
- Optional: Defaults to 1.
- Must be greater than the number of users available (you cannot request 5 approvals from a team of 4 users)
- teams: An array of team slugs that need to review this file.
- Optional if users is defined.
- users: An array of the GitHub usernames of the users who need to review this file.
- Optional if teams is defined.
- countAuthor: If the pull request author should be considered as an approval.
- If the author belongs to the list of approved users (either by team or by users) his approval will be counted (requiring one less approvals in total).
- ** Optional**: Defaults to
false
- allowedToSkipRule: If the author belong to one of the teams and/or users in the list, the rule should be skipped.
- Optional.
- This is useful for cases where we want to make sure that some eyes look into a PR, but for we don’t need to ensure that much security on internal teams.
- For example, if someone modifies a CI file, we want to make sure they didn’t break anything. Unless it’s someone from the CI team. They should know what they are doing.
The other three rules (or, and and and-distinct) have the same configuration, so let’s summarize that here and then move into how they work.
rules:
- name: Other rule
condition:
include:
- '.*'
exclude:
- 'example'
countAuthor: true
type: or | and | and-distinct
reviewers:
- teams:
- team-example
users:
- user-1
- user-2
- teams:
- team-abc
minApprovals: 2
- The name and conditions fields have the same requirements that the
basic
rule has. - type: Must be
or
,and
orand-distinct
. - countAuthor: If the pull request author should be considered as an approval.
- If the author belongs to the list of approved users (either by team or by users) his approval will be counted (requiring one less approvals in total).
- ** Optional**: Defaults to
false
- reviewers: This is an array that contains all the available options for review.
- Each of these options works independently.
- Must have at least two options.
- If you only need 1 option, then use the
basic
rule.
- If you only need 1 option, then use the
- Each of the options have the following fields:
- minApprovals: The number of approvals that are need to fulfill this condition.
- It can not be lower than 1.
- Optional: Defaults to 1.
- Must be greater than the number of users available (you cannot request 5 approvals from a team of 4 users)
- teams: An array of team slugs that need to review this file.
- Optional if users is defined.
- users: An array of the GitHub usernames of the users who need to review this file.
- Optional if teams is defined.
- minApprovals: The number of approvals that are need to fulfill this condition.
This is a rule that has at least two available options of reviewers and needs at least one group to approve.
If we look at the reviewers
field in the example above:
reviewers:
- teams:
- team-example
users:
- user-1
- user-2
- teams:
- team-abc
minApprovals: 2
This rule will be approved with any of the following conditions:
- If a user who belongs to
team-example
or isuser-1
oruser-2
approves the PR. - If two users who belong to
team-abc
approve the PR.
As you can see, this only requires one of the conditions to be fulfilled to approve the rule. You could approve the rule with only one user’s review instead of two. That’s why it is called the or
rule.
This is a rule that has at least two available options of reviewers and requires all the options to approve the PR.
If we look at the reviewers
field in the example above:
reviewers:
- teams:
- team-example
users:
- user-1
- user-2
- teams:
- team-abc
minApprovals: 2
This rule will be approved if all the following conditions get fulfilled:
- One user who belongs to
team-example
or isuser-1
oruser-2
approves the PR. - Two users who belong to
team-abc
approve the PR.
If only one of these conditions gets fulfilled, the check won’t pass, as it requires all the groups. You would need 3 approvals to fulfill all the conditions.
Although, there is a caveat with this rule.
In this example, if a user belongs to both team-abc
and to team-example
his approval will count towards both rules, so you could use only two approvals instead of three. To solve that, we created the and-distinct
rule.
The logic in this rule is the same as the and
rule but with one exception. Like the and
rule, it needs all of its requirements to be fulfilled, but they all must be fulfilled by different users.
Meaning that if a user belongs to both team-abc
and team-example
their approval will count only towards one of the available options even if they fulfill both needs.
This rule is useful when you need to make sure that at leasts two sets of eyes of two different teams review a Pull Request.
The fellows rule requires an extra collection to be added to the configuration and distinguishes itself from the other rules with some new fields:
rules:
- name: Fellows review
condition:
include:
- '.*'
exclude:
- 'example'
type: fellows
minRank: 2
minApprovals: 2
minTotalScore: 4
scores:
dan1: 1
dan2: 2
dan3: 4
dan4: 8
dan5: 12
dan6: 15
dan7: 20
dan8: 25
dan9: 30
The biggest difference in the rule configuration is that it doesn’t have a reviewers type (it doesn’t have a teams
or users
field); instead, it has a minRank
field and minTotalScore
field.
The minRank
field receives a number, which will be the lowest rank required to evaluate the PR, and then it fetches all the fellows from the chain data, filters only the one to belong to that rank or above and then looks into their metadata for a field name github
and the handle there.
The minTotalScore
field relies on the scores
collection. In this collection, each rank (dan) receives a score, so, in the example, a fellow dan 3 will have a score of 4. If the field minTotalScore
is enabled, the conditions to approve a pull request, aside from requiring a given amount of reviews from a given rank, will be that the total sum of all the scores from the fellows that have approved the pull request are equal or greater to the minTotalScore
field. This field is optional.
For every dan that has not been attributed a score, their score will default to 0.
After this is done, the resulting handles will be treated like a normal list of required users.
It also has any other field from the basic rule
(with the exception of users
and teams
):
- name
- conditions:
- include is required.
- exclude is optional.
- type: Must be
fellows
. - countAuthor: If the pull request author should be considered as an approval.
- Optional: Defaults to
false
.
- Optional: Defaults to
- minRank: Must be a number.
- Required
- minTotalScore: Must be a number
- Optional: Defaults to 0.
The fellows rule will never request reviewers, even if request-reviewers
rule is enabled.
This is because there are ~50 fellows and GitHub’s PR request limit is 20 users, so, if a low rank is required, the system wouldn’t allow to assign them.
If you want to evaluate the config file to find problems before merging it, we have a simple cli
to do so.
yarn run cli ".github/review-bot.yml" # set the parameter as the location of the config
It will inform you if you have any types of errors.
We use yarn
package manager and node 20
.
- Use
yarn install
to install the dependencies. - Use
yarn build
to build the project. - Use
yarn test
to run tests on all the*.test.ts
files. - Use
yarn lint
to run the linter. - Use
yarn fix
to fix all the auto fixable issues reported by the linter.
To deploy a new version you need to update two files:
package.json
: Update the version number.action.yml
: Update the image number inruns.image
. Important: Both versions must have the same number.
When a commit is pushed to the main branch and the versions have changed, the system will automatically tag the commit and release a new package with such version.
You can find all the available versions in the release section.