Expressions

PullApprove expressions allow you to write powerful, custom rules to design your workflow.

Expressions are evaluated in a Python environment, using the provided functions and variables. You can use human-readable operators like "in", "not in", "and", and, "or".

The same syntax powers the group conditions, overrides, and notification filters.

Single vs multi-line

Most basic expressions can be written in a single line. In YAML, you'll typically want to surround these with single-quotes since you'll have double-quotes inside (or vice versa):

version: 3
groups:
  api:
    conditions:
    - '"API" in title'

When you're comparing lots of values, it may be easier to write the expression over multiple lines using the > syntax:

version: 3
overrides:
- if: >
    not contains_any_fnmatches(files, [
      "packages/*",
      "apps/*",
      "docs/*",
      ])
  status: success

Examples

You can apply the same basic operations to most of the data we make available. Here are some common examples of how to use the available functions and variables.

Files

Basic string comparisons will use fnmatch under the hood.

# (GitHub)
"*.js" in files or "frontend/*" in files
# (Bitbucket)
"*.js" in diffstat or "frontend/*" in diffstat

You can also use the contains_any_fnmatch function to check more paths at once without writing lots of "or" lines.

# (GitHub)
contains_any_fnmatches(files, [
    "packages/*",
    "apps/*",
    "docs/*",
    ])
# (Bitbucket)
contains_any_fnmatches(diffstat, [
    "packages/*",
    "apps/*",
    "docs/*",
    ])

Or globs using contains_any_globs:

# (GitHub)
contains_any_globs(files, [
    "packages/animations/**",
    "packages/platform-browser/animations/**",
    "aio/content/guide/animations.md",
    "aio/content/examples/animations/**",
    "aio/content/images/guide/animations/**",
    "aio/content/guide/complex-animation-sequences.md",
    "aio/content/guide/reusable-animations.md",
    "aio/content/guide/route-animations.md",
    "aio/content/guide/transition-and-triggers.md",
    ])
# (Bitbucket)
contains_any_globs(diffstat, [
    "packages/animations/**",
    "packages/platform-browser/animations/**",
    "aio/content/guide/animations.md",
    "aio/content/examples/animations/**",
    "aio/content/images/guide/animations/**",
    "aio/content/guide/complex-animation-sequences.md",
    "aio/content/guide/reusable-animations.md",
    "aio/content/guide/route-animations.md",
    "aio/content/guide/transition-and-triggers.md",
    ])

Or a glob() instance:

# (GitHub)
glob("packages/**/*.js") in files
# (Bitbucket)
glob("packages/**/*.js") in diffstat

You can also chain the include and exclude methods to further filter down a list of files:

# (GitHub)
files.include("src/*").exclude("*.md")
# (Bitbucket)
diffstat.include("src/*").exclude("*.md")

Branches

Use branch variables to enable review depending on PR branch names or where it's being merged into:

# (GitHub)
# Reference a hardcoded branch name
base.ref == "master"

# Or another variable
base.ref != base.repo.default_branch

# Or use the standard comparision operators
"feature/" in head.ref
# (GitLab)
# Reference a hardcoded branch name
target_branch == "master"

# Or use the standard comparision operators
"feature/" in source_branch
# (Bitbucket)
# Reference a hardcoded branch name
destination.branch_name == "master"

# Or another variable
destination.branch_name != destination.repository.mainbranch.name

# Or use the standard comparision operators
"feature/" in source.branch_name

Labels

# (GitHub)
# Standard strings
"bug" in labels

# String patterns (fnmatch)
"sig-*" in labels

# Regular expressions
regex('.*/app') in labels
# (GitLab)
# Standard strings
"bug" in labels

# String patterns (fnmatch)
"sig-*" in labels

# Regular expressions
regex('.*/app') in labels

Other PullApprove groups

Use the state of preceding groups (above the current group in your config).

# (All platforms)
# Use list operators
len(groups.approved) > 3

# Or basic logic by name
"admins" in groups.passing

Checks and statuses

The "checks" on a PR come in two forms: the Checks API, and the commit Statuses API.

The GitHub interface often combines these, making it hard to tell the difference. The best way to tell the difference is to look for the Checks tab on a PR page. For anything on that page (like GitHub Actions/Workflows), use the check_runs object. If you have a status that is not on that page, it will be found in statuses.

GitHub pull request "Checks" tab

Checks

# (GitHub)
"build" not in check_runs.failed

Statuses

# (GitHub)
"*travis*" in statuses.succeeded
# (Bitbucket)
"*travis*" in statuses.successful

Title

# (All platforms)
# Use basic string comparisions
"WIP" in title

# Or fnmatch
"WIP*" in title

# Or regular expressions
regex("WIP: .*") in title

Body/description

# (GitHub)
"needs review" in body
# (GitLab)
"needs review" in description
# (Bitbucket)
"needs review" in description

Size

# (GitHub)
changed_files > 30
# (Bitbucket)
len(diffstat.paths) > 30

Author

# (All platforms)
author in ["internA", "internB"]

GitHub also has an author_association variable which can be used (check possible values here):

# (GitHub)
author_association == "FIRST_TIME_CONTRIBUTOR"
# (GitLab)
first_contribution

Mergeability

# (GitHub)
not mergeable
# (GitLab)
# https://docs.gitlab.com/ee/api/merge_requests.html#single-merge-request-response-notes
merge_status == "can_be_merged"

Git diff and files changed

# (GitHub)
contains_fnmatch(files.lines_added, '*dangerouslySetInnerHTML*')
# (GitLab)
contains_fnmatch(diff.lines_added, '*dangerouslySetInnerHTML*')
# (Bitbucket)
contains_fnmatch(diff.lines_added, '*dangerouslySetInnerHTML*')

Dates

# (GitHub)
created_at < date('3 days ago')
# (Bitbucket)
created_on < date('3 days ago')