DevSensei | Code Owners for Bitbucket

Expression Language specification

Expressions are small computations on Pull Request attributes. Expressions are most useful in workflow conditions, string templates, and custom merge checks. Expressions can be as simple as a constant (like a number or a string) or an attribute value, but expressions can also be more complex manipulation of attributes.

Some examples of expressions are

  • draft

  • true

  • 'Hello World'

  • 'PR ${title} opened by ${author}'

  • source ~= 'bugfix/*'

  • count(approved-by)

  • builds.successful

  • forall(changed-files, $1 ~= '**/*.js')

Expressions can be composed in any form or fashion using expression operators described below, as long as their type requirements are fulfilled. If an expression does not type-check (e.g., source > 3), it will make the devsensei.yaml file invalid.

Expressions can return any type, but expressions involved in conditions have to return a boolean. For example, even though count(approved-by) is a valid expression, it is not valid as part of the conditions.

String Template Literals

A literal string also supports templating. Arbitrary expressions can be spliced in with ${expr} if expr is convertible to string.

Template Examples
  • 'PR: ${title}' directly embeds the title attribute of type string.

  • 'Found ${count(commits)} commits' embeds the expression count(commits) of type int by automatic conversion to string.

String Conversions

Expressions of the following types may be directly inserted into a string template: int, boolean, string, regex (inserts the pattern).

Other types, such as duration, range, users, sets and arrays, cannot be inserted directly. Use join to turn a collection of strings into a single string, e.g. 'Keys: ${join(jira-keys, ', ')}'.

💡 We plan to support more data types in string templates soon. Let us know if you need this for your use case.

Lambda functions 🔬

Some functions require a lambda function as argument, for example exists or forall. A lambda function looks like a usual expression except $1 is used as a placeholder for the function argument.

For example, the following expressions are valid lambdas:

  • $1 ~= 'alice'

  • '${$1}' ~= 'alice' (the same via conversion.)

  • !$1

  • count($1) < 50

  • contains(reviewers.approved, $1)

As a complete example, the following expression checks that at least one reviewer is called "alice":

  • exists(reviewers, $1 = 'alice'),

in this expression reviewers is a collection of type set(pr-participant), and $1 is a placeholder that will be substituted with each reviewer in the collection until a true result is found.

since 9.8 Lambdas can be nested, and $1 may be used several times within one lambda. $1 always refers to the argument of the innermost lambda. For example:

  • exists(commits, forall($1.jira-keys, $1 ~= 'PROJ-*'))

  • map(changed-lines.added, '${$1.file-path}: ${$1.text}')

Only the single argument $1 is supported, and it may only be used inside a lambda.

Expression Syntax reference

Atomic expressions

Kind

Example

boolean literals

true or false

integer literals

7, -6, +2

string literals, enclosed in single quotes. Within quotes, the following escapes are supported: \' (single quote), \\ (backslash), \$ (dollar, e.g. to write a literal ${), \n (newline) and \t (tab). Any other backslash sequence is invalid, so a regex like \d must be written '\\d+'.

'Hello world', 'from: ${source}', 'it\'s'

unquoted strings: a single word without spaces or any of ( ) = < > ~. Only allowed on either side of =, != and of a glob match ~= / ~!=; elsewhere quotes are required. A word that is the name of an attribute is read as that attribute, not as a string.

destination = master, source ~= bugfix/*

an attribute reference

source, approved-by

lambda function argument reference

$1

wrapped expressions: '(' <expression> ')'

(true != false)

compound expressions

Kind

Syntax

Example

infix operator

<expression> <infix-op> <expression>

source ~= 'bugfix/*'

prefix operator

<prefix-op> <expression>

not draft

function call

<call-op> '(' <expression> [',' <expression> ]* ')'

count(open-tasks), contains(reviewers, 'user')

property access

<expression> '.' <property>

changed-files.modified

Whitespace

Outside of string literals, spaces are insignificant, with these exceptions:

  • no space between a function name and its opening parenthesis: count(commits), not count (commits)

  • no spaces around the . of a property access: builds.successful

  • the prefix not must be followed by a space: not draft

Combining infix operators

An infix operator takes exactly one operand on each side; infix operators cannot be chained. Use parentheses to combine them, e.g. (count(approved-by) > 1) = draft instead of count(approved-by) > 1 = draft. There are no and / or operators within an expression.

Expression types

Here are the following types that can be involved in expressions

Markers show the constraints a type satisfies, which some operators require of their arguments:

  • ⚖️ equatable: values can be compared with = and !=, and can be elements of a set.

  • 🔤 displayable: values can be inserted into a string template or joined with join.

Type name

Description

?0, ?1

type variables, used in the signatures of generic operators. Every occurrence of the same variable in one signature stands for the same type, e.g. (?0, ?0) → boolean compares two values of the same type. A variable may carry a constraint, e.g. where ?0 is equatable: only types with the matching marker (⚖️ equatable, 🔤 displayable) can be used for it.

boolean ⚖️ 🔤

a value that is one of either true or false.

string ⚖️ 🔤

any UTF-8 text value.

literal string ⚖️ 🔤

a string written directly in the expression, e.g. '2 days'. A literal string can be used anywhere a string is expected. Some functions, such as regex and duration, only accept a literal string, so that their argument can be validated when the configuration is loaded.

int ⚖️ 🔤

an integer value (between -2147483648 and 2147483647)

array(T) ⚖️ if T is

a sequence of elements of type T, may contain duplicates, e.g. array(commit).

set(T) ⚖️

an aggregate of unique elements of type T, e.g. set(string). Elements must be equatable.

user|git-author ⚖️

someone identified by a key: a platform user or a git author, e.g. the elements of approved-by and commits.authors. Compare with a string using = or contains; the string is matched against the user key (username or email). Has the field user-key.

user ⚖️

a registered application user, e.g. author and the elements of watchers. Can be used wherever user|git-author is expected.

pr-participant ⚖️

a user participating in the PR review, e.g. the elements of reviewers. Can be used wherever user is expected.

git-author ⚖️

the author of a commit, as recorded by git (identified by email). Can be used wherever user|git-author is expected.

lambda(from, to)

a lambda function expression taking an argument of type from and returning an argument of type to.

duration ⚖️

Value that represents a duration, such as 1 minute or 2 days. Durations can be created using the duration function.

regex ⚖️ 🔤

Regular expression object to be used for string comparisons (with the ~= operator) or the regex-extract function. Regular expressions can be created using the regex function. Regular expression in DevSensei use the re2 specification.

range ⚖️

Represents values between a lower bound (inclusive) and an upper bound (exclusive). Ranges can be created using the range function.

commit ⚖️

a commit in the PR, e.g. the elements of commits (see Properties).

line ⚖️

a line changed in the PR's diff, e.g. the elements of changed-lines (see Properties).

build ⚖️

a build reported for the PR's latest commit, e.g. the elements of builds (see Properties).

file-diff-info ⚖️

a file changed in the PR's diff, the $1 of a checklist file filter (see Properties).

task

an open task on the PR. Tasks can only be counted (e.g. count(open-tasks) = 0); they have no fields and cannot be compared.

Expression Operators

Use Operators to transform expressions into more complex ones. For example, to compare two int values or to count the number of elements in a set. Operators can be infix (only for binary operators), prefix or call. Infix operators are placed within their arguments, prefix operators must be placed before their arguments, and call operators must enclose their argument(s) in parenthesis, separated by commas.

Operator

Meaning

Type

Position

Example

~=

Matches a string against a pattern: a glob (branch pattern) given as a string, or a regex. A regex matches if the pattern occurs anywhere in the string.

On a collection of strings (e.g. changed-files, jira-keys, commits.titles), returns true when at least one element matches.

On changed lines since 9.1 , returns true when the content of at least one line matches the regex.

(string, string) → boolean

(string, regex) → boolean

(set(string), string) → boolean

(set(string), regex) → boolean

(array(string), string) → boolean

(array(string), regex) → boolean

(array(line), regex) → boolean

infix

source ~= 'bugfix/*'

title ~= regex('^(FIX|FEAT)')

changed-files.modified ~= '**/*.js'

changed-lines.added ~= regex('TODO')

~!=

Negation of glob or regex match (i.e. no matches)

Same signatures as ~=, e.g. (string, string) → boolean, (string, regex) → boolean, (set(string), string) → boolean

infix

title ~!= 'DRAFT*'

=

Equals

A user can be compared with a string holding a user key, with the arguments in either order: author = 'alice' and 'alice' = author are equivalent.

(?0, ?0) → boolean where ?0 is equatable

(user|git-author, string) → boolean

(string, user|git-author) → boolean

infix

destination = 'master', author = 'foo@example.com'

!=

Not equals

Same signatures as =

infix

count(open-tasks) != 0

! or not

Negate a boolean attribute (use of ! must be in a quoted string)

(boolean) → boolean

prefix

!draft

>, >=, < and <=

Number, string length or duration comparison

(int, int) → boolean, (string, int) → boolean, (duration, duration) → boolean

infix

5 >= 3, title <= 50, age > duration('2 days')

count

Computes the number of elements in a set or array, or the number of characters in a string

(set(?0)) → int where ?0 is equatable

(array(?0)) → int

(string) → int

call

count(approved-by), count('hello')

contains since 8.7

Checks whether a set of users contains a user matching the given user key, e.g. reviewers, approved-by, watchers or commits.authors. To check a set of strings, use exists(jira-keys, $1 = 'PROJ-1').

(set(user|git-author), string) → boolean

call

contains(reviewers, 'user1')

not

Negates a boolean expression

(boolean) → boolean

call

not(count(approved-by) > 3)

forall since 8.7

Checks whether the items in the first argument collection all satisfy the predicate specified by the lambda in the second argument. Always returns true for empty collections.

(set(?0), lambda(?0, boolean)) → boolean where ?0 is equatable

(array(?0), lambda(?0, boolean)) → boolean

call

forall(changed-files, $1 ~= regex('src/.*'))

exists since 8.7

Checks whether there is at least one item in the first argument collection that satisfy the predicate specified by the lambda in the second argument. Always returns false for empty collections.

(set(?0), lambda(?0, boolean)) → boolean where ?0 is equatable

(array(?0), lambda(?0, boolean)) → boolean

call

exists(reviewers, $1 = 'alice')

filter

since 9.2

Keeps only elements from the collection in the first argument that satisfy the predicate specified by the lambda in second argument.

(set(?0), lambda(?0, boolean)) → set(?0) where ?0 is equatable

(array(?0), lambda(?0, boolean)) → array(?0)

call

filter(changed-lines, $1.text ~= regex('<Form'))

map

since 9.2

Returns a new collection by transforming all elements in the first argument collection by the lambda in the second argument. Mapping a set returns a set, so duplicate results are removed; mapping an array keeps them.

(set(?0), lambda(?0, ?1)) → set(?1) where ?0 and ?1 are equatable

(array(?0), lambda(?0, ?1)) → array(?1)

call

map(changed-lines, $1.text)

join

Joins the elements of a collection into a single string, separated by the specified delimiter. Elements must be displayable (see String Conversions).

(set(?0), string) → string where ?0 is displayable and equatable

(array(?0), string) → string where ?0 is displayable

call

join(commits.titles, ', ')

range

Creates a range between the specified bounds (lower is inclusive, upper is exclusive)

(int, int) → range

call

range(5, 7) represents the numbers 5 and 6.

slice

since 9.3

Returns the part of the string in the first argument with characters taken from the range specified as second argument.

If the range is empty (i.e., lower bound bigger than or equal to the upper bound), then returns the empty string.

(string, range) → string

call

slice('hello world', range(6, 11)) returns 'world'.

duration

Creates a duration from its expression. A duration expression is a number followed by a time unit, with or without a space, e.g. s, min, mins, minute(s), h, hour(s), d, day(s). The argument must be a literal string; an invalid duration makes the configuration invalid.

(literal string) → duration

call

duration('2 hours')

regex

Creates a regular expression from its string pattern representation. The pattern must be a literal string, e.g. regex(title) is invalid. The regex matches if the pattern occurs anywhere in the string; use ^ and $ to anchor it.

If the pattern is not valid, the DevSensei configuration will be invalid as well. Please refer to the re2 regex specification for its syntax.

(literal string) → regex

call

regex('[0-9]+')

regex-extract

since 9.3

Uses a regular expression with a matching group to extract its value within the string given as first argument.

The regular expression must have exactly 1 defined group, otherwise the expression is invalid.

If the regular expression does not match the string, this function returns the empty string.

(string, regex) → string

call

regex-extract(source, regex('feature-([A-Z]+-[0-9]+)/.*'))

Attributes🔰

These are the various attributes available to use in conditions, retrigger-on, and merge-checks. Each attribute results in a typed value when evaluated.

Attributes labelled with the lightning emoji ⚡️ can be used in the retrigger-on section. Each retrigger-on entry must be exactly one attribute name, not an expression.

Condition Attribute

Type

Meaning

title ⚡️

string

The pull request title

description ⚡️

string

The pull request description

source ⚡️

string

Source branch of pull request

destination ⚡️

string

Destination branch of pull request

repository-name ⚡️

string

Destination repository of pull request

source-head-sha ⚡️

string

The SHA-1 hash of commit of the HEAD ref on the source branch

draft ⚡️

boolean

Is it a draft pull request

conflicts⚡️

boolean

Whether the Pull Request currently has conflicts with the target branch.

author

since 8.6

user

The user who created the PR.

age

since 8.11

duration

Time since the pull request was opened. Conditions involving the age can be configured via a schedule.

open-tasks

array(task)

The tasks that are still open in the PR. Tasks can only be counted, e.g. count(open-tasks) = 0.

approved-by

set(user|git-author)

The set of users who have approved the PR. Compare with a user key, e.g. contains(approved-by, 'alice').

reviewers

set(pr-participant)

The set of users who are reviewers of the PR and their review status. Has properties .approved, .changes-requested and .waiting.

changed-files

since 8.6

set(string)

The set of paths of all files that are affected by the pull request. Has properties .added, .deleted, .modified and .renamed.

changed-lines

since 9.1

array(line)

The list of all changed lines (added or removed) that are affected by the pull request.

Has properties .added and .removed. Unlike files, lines are not modified (one is removed and the replacement is added).

builds

since 8.6

set(build)

The set of builds for the most recent commit for the pull request. Has properties .successful, .failed and .in-progress.

watchers

since 8.7

set(user)

The set of users who are watchers of the PR. Watchers are people that interacted with the pull request. See Bitbucket documentation.

commits

since 8.7

array(commit)

List of commits in this PR. Has properties .titles, .messages and .authors. Each commit also has a jira-keys property.

jira-keys

since 8.7

set(string)

Set of Jira keys mentioned in all commit messages.

diff-change ⚡️

n/a

Only usable in retrigger-on: re-triggers when the diff of the PR changes. Cannot be used in expressions.

Attribute and data types Properties 🔬

Some attributes have properties, (for use in property access expressions) that provide a view over the contained data.

attribute/data type

Property

Meaning

builds

successful

The successful builds


failed

The failed builds


in-progress

The builds that are in progress

build

name

The name of the build. Eg exists(builds.successful, $1.name = 'my-build')


key

The key of the build

changed-files

added

Only the new files that were not seen before


deleted

Only the files that will be deleted after merging


modified

Only the files that changed (and possibly also moved).


renamed

Only the files that were not changed, but were moved

changed-lines

added

Only the added lines


removed

Only the removed lines

reviewers

approved

Only the reviewers who have approved the PR.


changes-requested

Only the reviewers who have requested changes on the PR.


waiting

Only the reviewers who have not completed their review.

user, pr-participant, git-author

user-key

The key identifying the person (user key, or email for a git author). Eg exists(reviewers.approved, $1.user-key ~= 'team-*')

commits

titles

array(string): the titles of all commits.


messages

array(string): the full messages of all commits.


authors

set(user|git-author): the authors of all the commits.

commit

title

The title of the commit message (the first line)


jira-keys (per commit only)

set of Jira keys in commit. Eg forall(commits, count($1.jira-keys) > 0) to ensure all commits have a Jira key

line

since 9.1

text

The content of the line

Eg forall(changed-lines.added, count($1.text) <= 80) ensures that no line is bigger than 80 characters.


file-path

The path (from repository root) of the line


added

Whether the line was added within the PR


removed

Whether the line was removed within the PR

file-diff-info

since 9.2

path

The path (from repository root) of the file in the PR diff. A file-diff-info is the $1 of a checklist file filter, e.g. $1.path ~= 'src/**' keeps only files under src/. (The elements of changed-files are plain path strings.)


added

Whether the file was added in the PR


deleted

Whether the file was deleted in the PR


modified

Whether the file was modified in the PR


line-changes

since 9.3

The changed lines of this file, with properties .added and .removed. Eg $1.line-changes.added ~= regex('TODO')